SPB Git

spb/autoscholar

Public
4.4 KB · 115 lines tsx
1import { getInstructionVersion, getProject, listRuns } from "@autoscholar/core";2import Link from "next/link";3import { notFound } from "next/navigation";4import { CodeBlock } from "@/components/CodeBlock";5import { MarkdownView } from "@/components/MarkdownView";6import { StatusBadge } from "@/components/StatusBadge";7import { disciplineLabel, fmtDateTime, fmtUsd } from "@/lib/format";8import { db } from "@/lib/server/db";910export const dynamic = "force-dynamic";1112export default async function ProjectDetail({13  params,14  searchParams,15}: {16  params: Promise<{ id: string }>;17  searchParams: Promise<{ v?: string }>;18}) {19  const { id } = await params;20  const { v } = await searchParams;21  const d = db();22  let data: Awaited<ReturnType<typeof getProject>>;23  try {24    data = await getProject(d, id);25  } catch {26    notFound();27  }28  const runs = await listRuns(d, { projectId: id, limit: 50 });29  const selected = v30    ? await getInstructionVersion(d, v).catch(() => null)31    : data.versions[0]32      ? await getInstructionVersion(d, data.versions[0].id)33      : null;34  return (35    <div className="space-y-6">36      <header>37        <p className="eyebrow">38          <Link href="/admin/projects">Projects</Link>39        </p>40        <h1 className="title-2 mt-1">{data.project.title}</h1>41        <p className="muted text-sm">42          {disciplineLabel(data.project.discipline)} · {data.project.description || "no description"}43        </p>44      </header>45      <div className="grid gap-6 lg:grid-cols-12">46        <section className="surface p-4 lg:col-span-4">47          <h2 className="title-3">Instruction versions</h2>48          <ul className="mt-2 space-y-2 text-sm">49            {data.versions.length === 0 && <li className="muted">None uploaded.</li>}50            {data.versions.map((iv) => (51              <li52                key={iv.id}53                className={`rounded-md border p-2 hairline ${selected?.id === iv.id ? "bg-[var(--bg-muted)]" : ""}`}54              >55                <Link href={`/admin/projects/${id}?v=${iv.id}`}>56                  v{iv.version} · {iv.filename}57                </Link>58                <p className="subtle font-mono text-xs">59                  {iv.sha256.slice(0, 16)} · {iv.byteSize} B · {fmtDateTime(iv.createdAt)}60                </p>61                {iv.note && <p className="muted text-xs">{iv.note}</p>}62                <Link href={`/admin/new?project=${id}`} className="btn btn-sm mt-2">63                  Re-run / duplicate64                </Link>65              </li>66            ))}67          </ul>68          <h2 className="serif mt-6 text-lg font-semibold">Runs</h2>69          <ul className="mt-2 space-y-1 text-sm">70            {runs.length === 0 && <li className="muted">No runs.</li>}71            {runs.map((r) => (72              <li key={r.id} className="flex items-center justify-between gap-2">73                <Link href={`/admin/runs/${r.id}`}>{r.title}</Link>74                <span className="flex items-center gap-2">75                  <StatusBadge status={r.status} />76                  <span className="subtle text-xs">{fmtUsd(r.totalCostUsd)}</span>77                </span>78              </li>79            ))}80          </ul>81        </section>82        <section className="lg:col-span-8">83          {selected ? (84            <div className="space-y-3">85              <div className="flex flex-wrap items-center justify-between gap-2">86                <h2 className="title-3">Instruction v{selected.version} (immutable)</h2>87                <span className="font-mono text-xs break-all">{selected.sha256}</span>88              </div>89              {selected.frontmatter && (90                <CodeBlock91                  code={JSON.stringify(selected.frontmatter, null, 2)}92                  lang="json"93                  maxHeight="10rem"94                />95              )}96              <details open className="surface p-4">97                <summary className="cursor-pointer text-sm">Rendered preview (sanitized)</summary>98                <div className="mt-3">99                  <MarkdownView markdown={selected.contentText} />100                </div>101              </details>102              <details className="surface p-4">103                <summary className="cursor-pointer text-sm">Raw text</summary>104                <CodeBlock code={selected.contentText} lang="md" />105              </details>106            </div>107          ) : (108            <p className="muted">Select a version.</p>109          )}110        </section>111      </div>112    </div>113  );114}115
Files · main
Menu

Theme